﻿/*
Copyright (c) 2008 William Duff

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using LiveUpload.Facebook.Properties;
using System.Diagnostics;
using System.Threading;
using System.Drawing.Drawing2D;
using Microsoft.WindowsLive.PublishPlugins;
using System.IO;

namespace LiveUpload.Facebook
{
    public partial class ConfigurationSettings : Form
    {
        #region Constructors

        /// <summary>
        /// Constructs a new ConfigurationSettings object to display a configuration form to the 
        /// user.
        /// </summary>
        /// <param name="sessionData">
        /// The data associated with this session.
        /// </param>
        /// <param name="persistData">
        /// The data associated with this application.
        /// </param>
        public ConfigurationSettings(SessionData sessionData, PersistentData persistData, IPublishProperties publishProperties)
        {
            InitializeComponent();

            this.sessionData = sessionData;
            this.persistData = persistData;
            this.publishProperties = publishProperties;

            this.uploadingImages = sessionData.ItemSet.Exists(delegate(Item mediaObject)
            {
                return mediaObject.PerceivedType == "image";
            });

            this.uploadingVideos = sessionData.ItemSet.Exists(delegate(Item mediaObject)
            {
                return mediaObject.PerceivedType == "video";
            });
        }

        #endregion

        #region Private Fields

        private SessionData sessionData;
        private PersistentData persistData;
        private IPublishProperties publishProperties;
        private int numAccountWorkers;
        private Account selectedUser;
        private Album selectedAlbum;
        private FacebookAccountRadioButton chooseAccountAddNew;
        private FacebookAlbumRadioButton chooseAlbumCreateNew;
        private string authToken;
        private bool uploadingImages;
        private bool uploadingVideos;
        private Account loadedAlbumsUser;
        private Account loadedLinkAccountsUser;

        private enum ConfigurationSettingsPanel
        {
            ChooseAccount,
            PreAuth,
            PostAuth,
            ChooseAlbum,
            LinkAccounts,
            VideoDesc,
            Publish
        }

        private const int TOP_PADDING = 5;
        private const int BOTTOM_PADDING = 10;
        private const int SIDE_PADDING = 5;

        #endregion

        #region Private Methods

        /// <summary>
        /// This event is fired when the form is first loaded.
        /// </summary>
        private void ConfigurationSettings_Load(object sender, EventArgs e)
        {
            // If there are saved users, asynchronously load their information.
            if (this.persistData.Users.Count > 0)
            {
                GetAccountInformation();
            }
            else
            {
                PopulateChooseAccountFlowLayoutPanel();
            }

            if (uploadingVideos)
            {
                PopulateVideoDescLayoutPanel();
            }

            PopulatePublishPanel();

            // Always start by choosing an account.
            ShowPanel(ConfigurationSettingsPanel.ChooseAccount);
        }

        /// <summary>
        /// Hides all of the panels except the specified one.
        /// </summary>
        /// <param name="configurationSettingsPanel">
        /// The panel to show.
        /// </param>
        private void ShowPanel(ConfigurationSettingsPanel configurationSettingsPanel)
        {
            this.SuspendLayout();

            this.chooseAccountPanel.Hide();
            this.chooseAlbumPanel.Hide();
            this.preAuthPanel.Hide();
            this.postAuthPanel.Hide();
            this.linkAccountsPanel.Hide();
            this.videoDescPanel.Hide();
            this.publishPanel.Hide();

            switch (configurationSettingsPanel)
            {
                case ConfigurationSettingsPanel.ChooseAccount:
                    this.AcceptButton = this.chooseAccountNextButton;
                    this.CancelButton = this.chooseAccountCancelButton;
                    this.chooseAccountPanel.Show();
                    this.chooseAccountNextButton.Focus();
                    break;
                case ConfigurationSettingsPanel.ChooseAlbum:
                    this.AcceptButton = this.chooseAlbumNextButton;
                    this.CancelButton = this.chooseAlbumCancelButton;
                    this.chooseAlbumPanel.Show();
                    this.chooseAlbumNextButton.Focus();
                    break;
                case ConfigurationSettingsPanel.PreAuth:
                    this.AcceptButton = this.preAuthNextButton;
                    this.CancelButton = this.preAuthCancelButton;
                    this.preAuthPanel.Show();
                    this.preAuthNextButton.Focus();
                    break;
                case ConfigurationSettingsPanel.PostAuth:
                    this.AcceptButton = this.postAuthNextButton;
                    this.CancelButton = this.postAuthCancelButton;
                    this.postAuthPanel.Show();
                    this.postAuthNextButton.Focus();
                    break;
                case ConfigurationSettingsPanel.LinkAccounts:
                    this.AcceptButton = this.linkAccountsNextButton;
                    this.CancelButton = this.linkAccountsCancelButton;
                    this.linkAccountsPanel.Show();
                    this.linkAccountsNextButton.Focus();
                    break;
                case ConfigurationSettingsPanel.VideoDesc:
                    this.AcceptButton = this.videoDescNextButton;
                    this.CancelButton = this.videoDescCancelButton;
                    this.videoDescPanel.Show();
                    this.videoDescNextButton.Focus();
                    break;
                case ConfigurationSettingsPanel.Publish:
                    this.AcceptButton = this.publishNextButton;
                    this.CancelButton = this.publishCancelButton;
                    this.publishPanel.Show();
                    this.publishNextButton.Focus();
                    break;
            }

            this.ResumeLayout(true);
        }

        /// <summary>
        /// Displays a message box with the given error message. If the error is fatal, closes the
        /// form.
        /// </summary>
        /// <param name="errorMessage">
        /// The message to display to the user.
        /// </param>
        /// <param name="isFatalError">
        /// true if the error cannot be recovered from and false otherwise.
        /// </param>
        private void DisplayError(string errorMessage, bool isFatalError)
        {
            MessageBox.Show(errorMessage, Resources.PluginTitle, MessageBoxButtons.OK,
                MessageBoxIcon.Error);

            if (isFatalError)
            {
                CloseForm(DialogResult.Cancel);
            }
        }

        /// <summary>
        /// Asynchronously loads all of the information about each saved user from Facebook.
        /// </summary>
        private void GetAccountInformation()
        {
            // Grab the current list of users.
            Account[] users = this.persistData.Users.ToArray();
            this.persistData.Users.Clear();

            // Start a background worker for each user to get information about them.
            foreach (Account user in users)
            {
                BackgroundWorker accountWorker = new BackgroundWorker();
                accountWorker.DoWork += new DoWorkEventHandler(accountWorker_DoWork);
                accountWorker.RunWorkerCompleted +=
                    new RunWorkerCompletedEventHandler(accountWorker_RunWorkerCompleted);
                accountWorker.RunWorkerAsync(user);

                this.numAccountWorkers++;
            }
        }

        /// <summary>
        /// Gets information about a given user. All the work is done on a background worker thread 
        /// so the UI thread is not tied up.
        /// </summary>
        private void accountWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            Account user = (Account)e.Argument;
            user = FacebookService.GetUserInfo(user.Session);

            e.Result = user;
        }

        /// <summary>
        /// Once the background worker returns, the UI thread runs this function. If there was no
        /// error, the user must still have a valid session and returned a list of albums.
        /// </summary>
        private void accountWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // This is thread safe because this function is run on the UI thread instead of the
            // background worker thread.
            this.numAccountWorkers--;

            // Make sure there was no exception and that the background thread wasn't cancelled.
            if (e.Error == null && !e.Cancelled)
            {
                AddUserToUserList((Account)e.Result);
            }

            // If all of the background worker threads have finished, populate the account panel.
            if (this.numAccountWorkers == 0)
            {
                PopulateChooseAccountFlowLayoutPanel();
            }
        }

        /// <summary>
        /// Adds a user to the list of saved users, complete with all of their Facebook information.
        /// </summary>
        /// <param name="user">
        /// The account to add.
        /// </param>
        private void AddUserToUserList(Account user)
        {
            int userIndex = this.persistData.Users.IndexOf(user);
            if (userIndex == -1)
            {
                // If the user was not found in the list, add them.
                this.persistData.Users.Add(user);
            }
            else
            {
                // If the user was found in the list, update them.
                this.persistData.Users[userIndex] = user;
            }
        }

        /// <summary>
        /// Removes a user account from the list of saved users.
        /// </summary>
        /// <param name="user">
        /// The account to remove.
        /// </param>
        private void RemoveUserFromUserList(Account user)
        {
            if (this.persistData.Users.Contains(user))
            {
                this.persistData.Users.Remove(user);
            }
        }

        /// <summary>
        /// Creates an invisible control that acts as a spacer.
        /// </summary>
        /// <returns>
        /// An invisible control that acts as a spacer.
        /// </returns>
        private Control CreateSpacer(int height)
        {
            Label spacer = new Label();
            spacer.Height = height;
            spacer.BackColor = Color.Transparent;

            return spacer;
        }

        /// <summary>
        /// Fills the account panel with a radio button for each existing account.
        /// </summary>
        private void PopulateChooseAccountFlowLayoutPanel()
        {
            // This function may get called multiple times, so clear any existing controls.
            this.chooseAccountFlowLayoutPanel.Controls.Clear();
            this.chooseAccountFlowLayoutPanel.UseWaitCursor = false;

            // Add top spacer
            Control spacer = CreateSpacer(TOP_PADDING);
            this.chooseAccountFlowLayoutPanel.Controls.Add(spacer);
            this.chooseAccountFlowLayoutPanel.SetFlowBreak(spacer, true);

            // This radio button will represent the option to add an account by default.
            chooseAccountAddNew = new FacebookAccountRadioButton();
            chooseAccountAddNew.CheckedChanged +=
                new EventHandler(accountRadioButton_CheckedChanged);
            chooseAccountAddNew.DoubleClick +=
                new EventHandler(accountRadioButton_DoubleClick);
            this.chooseAccountFlowLayoutPanel.Controls.Add(chooseAccountAddNew);
            this.chooseAccountFlowLayoutPanel.SetFlowBreak(chooseAccountAddNew, true);

            if (chooseAccountAddNew.User == this.selectedUser)
            {
                chooseAccountAddNew.Select();
            }

            // Add each user to the account panel as their own radio button.
            FacebookAccountRadioButton accountRadioButton;

            foreach (Account user in this.persistData.Users)
            {
                accountRadioButton = new FacebookAccountRadioButton();
                accountRadioButton.User = user;
                accountRadioButton.CheckedChanged +=
                    new EventHandler(accountRadioButton_CheckedChanged);
                accountRadioButton.DoubleClick +=
                    new EventHandler(accountRadioButton_DoubleClick);
                accountRadioButton.RemoveClicked +=
                    new EventHandler(accountRadioButton_RemoveClicked);
                this.chooseAccountFlowLayoutPanel.Controls.Add(accountRadioButton);
                this.chooseAccountFlowLayoutPanel.SetFlowBreak(accountRadioButton, true);

                if (accountRadioButton.User == this.selectedUser)
                {
                    accountRadioButton.Select();
                }
            }

            // Add bottom spacer
            spacer = CreateSpacer(BOTTOM_PADDING);
            this.chooseAccountFlowLayoutPanel.Controls.Add(spacer);
            this.chooseAccountFlowLayoutPanel.SetFlowBreak(spacer, true);
        }

        /// <summary>
        /// This event is fired when an account is selected or deselected.
        /// </summary>
        private void accountRadioButton_CheckedChanged(object sender, EventArgs e)
        {
            FacebookAccountRadioButton accountRadioButton = sender as FacebookAccountRadioButton;
            if (accountRadioButton != null && accountRadioButton.Checked)
            {
                SetSelectedUser(accountRadioButton.User);
            }
        }

        /// <summary>
        /// This event is fired when an account is double clicked.
        /// </summary>
        private void accountRadioButton_DoubleClick(object sender, EventArgs e)
        {
            FacebookAccountRadioButton accountRadioButton = sender as FacebookAccountRadioButton;
            if (accountRadioButton != null)
            {
                // Double clicking automatically selects the user that was double clicked and 
                // proceeds to the next panel.
                SetSelectedUser(accountRadioButton.User);
                chooseAccountNextButton_Click(sender, e);
            }
        }

        /// <summary>
        /// Sets the user that is selected to upload photos with.
        /// </summary>
        /// <param name="user">
        /// The account to select.
        /// </param>
        private void SetSelectedUser(Account user)
        {
            this.selectedUser = user;

            // Once we've made the user selection, make sure to show them their upload limits.
            if (uploadingVideos && user != null && user.VideoUploadLimits != null)
            {
                videoDescLabel.Text = string.Format(videoDescLabel.Text, user.VideoUploadLimits);
            }

            this.chooseAccountNextButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the remove button of an account is clicked.
        /// </summary>
        private void accountRadioButton_RemoveClicked(object sender, EventArgs e)
        {
            FacebookAccountRadioButton accountRadioButton = sender as FacebookAccountRadioButton;
            if (accountRadioButton != null)
            {
                string question =
                    string.Format(Resources.RemoveAccountQuestion, accountRadioButton.User.Name);
                
                // Pop a dialog to confirm that the user wants to remove the account.
                DialogResult removeAccount = MessageBox.Show(question, Resources.PluginTitle,
                    MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
                
                if (removeAccount == DialogResult.Yes)
                {
                    if (this.selectedUser == accountRadioButton.User)
                    {
                        SetSelectedUser(null);
                    }

                    RemoveUserFromUserList(accountRadioButton.User);
                    PopulateChooseAccountFlowLayoutPanel();
                }
            }
        }

        /// <summary>
        /// Fills the album panel with a radio button for each existing album owned by the selected
        /// user.
        /// </summary>
        private void PopulateChooseAlbumFlowLayoutPanel()
        {
            if (this.chooseAlbumFlowLayoutPanel.Controls.Count > 0 && selectedUser == loadedAlbumsUser)
            {
                // If we already loaded this panel for the selected user, don't reload it.
                return;
            }
            else
            {
                // Save who we loaded this panel for.
                loadedAlbumsUser = selectedUser;
            }

            // This function may get called multiple times, so clear any existing controls.
            this.chooseAlbumFlowLayoutPanel.Controls.Clear();

            // Add top spacer
            Control spacer = CreateSpacer(TOP_PADDING);
            this.chooseAlbumFlowLayoutPanel.Controls.Add(spacer);
            this.chooseAlbumFlowLayoutPanel.SetFlowBreak(spacer, true);

            // This radio button will represent the option to create a new album by default.
            this.chooseAlbumCreateNew = new FacebookAlbumRadioButton();
            this.chooseAlbumCreateNew.CheckedChanged += 
                new EventHandler(albumRadioButton_CheckedChanged);
            this.chooseAlbumCreateNew.DoubleClick += 
                new EventHandler(chooseAlbumRadioButton_DoubleClick);
            this.chooseAlbumFlowLayoutPanel.Controls.Add(this.chooseAlbumCreateNew);
            this.chooseAccountFlowLayoutPanel.SetFlowBreak(this.chooseAlbumCreateNew, true);
            this.chooseAlbumCreateNew.Select();

            // Add each album to the album panel as their own radio button.
            foreach (Album album in this.selectedUser.Albums)
            {
                FacebookAlbumRadioButton albumRadioButton = new FacebookAlbumRadioButton();
                albumRadioButton.Album = album;
                albumRadioButton.CheckedChanged += 
                    new EventHandler(albumRadioButton_CheckedChanged);
                albumRadioButton.DoubleClick += 
                    new EventHandler(chooseAlbumRadioButton_DoubleClick);
                this.chooseAlbumFlowLayoutPanel.Controls.Add(albumRadioButton);
                this.chooseAlbumFlowLayoutPanel.SetFlowBreak(albumRadioButton, true);
            }

            // Add bottom spacer
            spacer = CreateSpacer(BOTTOM_PADDING);
            this.chooseAlbumFlowLayoutPanel.Controls.Add(spacer);
            this.chooseAlbumFlowLayoutPanel.SetFlowBreak(spacer, true);
        }

        /// <summary>
        /// This event is fired when an album is selected or deselected.
        /// </summary>
        private void albumRadioButton_CheckedChanged(object sender, EventArgs e)
        {
            FacebookAlbumRadioButton albumRadioButton = sender as FacebookAlbumRadioButton;
            if (albumRadioButton != null && albumRadioButton.Checked)
            {
                SetSelectedAlbum(albumRadioButton.Album);
            }
        }

        /// <summary>
        /// This event is fired when an album is double clicked.
        /// </summary>
        private void chooseAlbumRadioButton_DoubleClick(object sender, EventArgs e)
        {
            FacebookAlbumRadioButton albumRadioButton = sender as FacebookAlbumRadioButton;
            if (albumRadioButton != null)
            {
                // Double clicking automatically selects the album that was double clicked and 
                // proceeds to the next panel.
                SetSelectedAlbum(albumRadioButton.Album);
                chooseAlbumNextButton_Click(sender, e);
            }
        }

        /// <summary>
        /// Sets the album that is selected to upload photos to.
        /// </summary>
        /// <param name="user">
        /// The album to select.
        /// </param>
        private void SetSelectedAlbum(Album album)
        {
            this.selectedAlbum = album;
            this.chooseAlbumNextButton.Enabled = true;
        }

        /// <summary>
        /// Fills the linkAccounts panel with a column of people tags and a corresponding
        /// column of Facebook friends.
        /// </summary>
        private void PopulateLinkAccountsLayoutPanel()
        {
            if (this.linkAccountsLayoutPanel.Controls.Count > 0 && selectedUser == loadedLinkAccountsUser)
            {
                // If we already loaded this panel for the selected user, don't reload it.
                return;
            }
            else
            {
                // Save who we loaded this panel for.
                loadedLinkAccountsUser = selectedUser;
            }

            // This function may get called multiple times, so clear any existing controls.
            this.linkAccountsLayoutPanel.Controls.Clear();

            Dictionary<string, AccountLink> accountLinks = new Dictionary<string, AccountLink>();
            Dictionary<string, PersonTag> uniquePeopleTags = 
                new Dictionary<string, PersonTag>(this.sessionData.UniquePeopleTags);

            // Try to find exact matches between saved account links and people tags.
            foreach (AccountLink savedAccountLink in this.persistData.AccountLinks)
            {
                PersonTag matchingPersonTag;
                string key = savedAccountLink.PersonTag.Name.ToLower();
                if (uniquePeopleTags.TryGetValue(key, out matchingPersonTag))
                {
                    accountLinks[key] = new AccountLink(matchingPersonTag, savedAccountLink.FacebookFriend);
                    uniquePeopleTags.Remove(key);
                }
            }

            // Try to find exact matches between facebook friends and people tags.
            foreach (Friend facebookFriend in this.selectedUser.FriendList)
            {
                PersonTag matchingPersonTag;
                string key = facebookFriend.Name.ToLower();
                if (uniquePeopleTags.TryGetValue(key, out matchingPersonTag))
                {
                    accountLinks[key] = new AccountLink(matchingPersonTag, facebookFriend);
                    uniquePeopleTags.Remove(key);
                }
            }

            // Add the remaining unique people tags as unlinked accounts.
            foreach (KeyValuePair<string, PersonTag> personTagPair in uniquePeopleTags)
            {
                string key = personTagPair.Value.Name.ToLower();
                accountLinks[key] = new AccountLink(personTagPair.Value);
            }

            List<FacebookAccountLinkPanel> accountLinkButtons = 
                new List<FacebookAccountLinkPanel>();

            // Add top spacer
            Control spacer = CreateSpacer(TOP_PADDING);
            this.linkAccountsLayoutPanel.Controls.Add(spacer);
            this.linkAccountsLayoutPanel.SetFlowBreak(spacer, true);

            // Now that we have our list of AccountLinks, add them to the LayoutPanel.
            foreach (KeyValuePair<string, AccountLink> accountLinkPair in accountLinks)
            {
                FacebookAccountLinkPanel accountLinkButton = new FacebookAccountLinkPanel();
                accountLinkButton.AccountLink = accountLinkPair.Value;
                accountLinkButton.FacebookFriends = this.selectedUser.FriendList;
                accountLinkButtons.Add(accountLinkButton);
                this.linkAccountsLayoutPanel.Controls.Add(accountLinkButton);
            }

            // Select the first choice.
            if(accountLinkButtons.Count > 0) 
            {
                accountLinkButtons[0].Select();
            }

            // Add bottom spacer
            spacer = CreateSpacer(BOTTOM_PADDING);
            this.linkAccountsLayoutPanel.Controls.Add(spacer);
            this.linkAccountsLayoutPanel.SetFlowBreak(spacer, true);

            // Create the person tag thumbnails in a background thread to keep the UI responsive.
            BackgroundWorker thumbnailWorker = new BackgroundWorker();
            thumbnailWorker.DoWork += new DoWorkEventHandler(thumbnailWorker_DoWork);
            thumbnailWorker.RunWorkerCompleted +=
                new RunWorkerCompletedEventHandler(thumbnailWorker_RunWorkerCompleted);
            thumbnailWorker.RunWorkerAsync(accountLinkButtons);
        }

        /// <summary>
        /// Creates a thumbnail of the given person tag. All the work is done on a background 
        /// worker thread so the UI thread is not tied up.
        /// </summary>
        private void thumbnailWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            List<FacebookAccountLinkPanel> accountLinkButtons = 
                e.Argument as List<FacebookAccountLinkPanel>;
            
            if (accountLinkButtons == null || accountLinkButtons.Count <= 0)
            {
                e.Result = null;
                return;
            }

            // We want to figure out if any person tags are in the same image so 
            // that we open as few images as possible.
            Dictionary<string, List<FacebookAccountLinkPanel>> imagePeopleTags =
                new Dictionary<string, List<FacebookAccountLinkPanel>>();

            foreach (FacebookAccountLinkPanel accountLinkButton in accountLinkButtons)
            {
                string key = accountLinkButton.AccountLink.PersonTag.ImagePath;
                if (key != string.Empty)
                {
                    List<FacebookAccountLinkPanel> savedAccountLinkButtons;

                    if (!imagePeopleTags.TryGetValue(key, out savedAccountLinkButtons))
                    {
                        savedAccountLinkButtons = new List<FacebookAccountLinkPanel>();
                    }

                    savedAccountLinkButtons.Add(accountLinkButton);
                    imagePeopleTags[key] = savedAccountLinkButtons;
                }
            }

            // Now we can start creating thumbnails.
            Dictionary<FacebookAccountLinkPanel, Bitmap> thumbnails =
                new Dictionary<FacebookAccountLinkPanel, Bitmap>();

            foreach (KeyValuePair<string, List<FacebookAccountLinkPanel>> imagePeopleTagsPair 
                in imagePeopleTags)
            {
                string imagePath = imagePeopleTagsPair.Key;
                List<FacebookAccountLinkPanel> savedAccountLinkButtons = imagePeopleTagsPair.Value;

                // Open the original image as a bitmap.
                Image openImage = Image.FromFile(imagePath);
                Bitmap original = new Bitmap(openImage);
                openImage.Dispose();

                foreach (FacebookAccountLinkPanel accountLinkButton in savedAccountLinkButtons)
                {
                    // Find the tag area (plus a small margin around it).
                    PersonTag personTag = accountLinkButton.AccountLink.PersonTag;
                    float aspectRatio = (float)original.Width / (float)original.Height;
                    float margin = 20.0f;
                    int x = (int)(Math.Max(personTag.Left * original.Width - margin * aspectRatio, 0.0f));
                    int y = (int)(Math.Max(personTag.Top * original.Height - margin * aspectRatio, 0.0f));
                    int width = (int)(Math.Min(personTag.Width * original.Width + 2 * margin * aspectRatio, original.Width - x));
                    int height = (int)(Math.Min(personTag.Height * original.Height + 2 * margin * aspectRatio, original.Height - y));
                    Rectangle tagArea = new Rectangle(x, y, width, height);

                    // Create the thumbnail.
                    int thumbnailWidth = accountLinkButton.PersonTagSquarePicture.Width;
                    int thumbnailHeight = accountLinkButton.PersonTagSquarePicture.Height;
                    Bitmap thumbnail = new Bitmap(thumbnailWidth, thumbnailHeight);
                    Graphics g = Graphics.FromImage(thumbnail);
                    g.InterpolationMode = InterpolationMode.HighQualityBicubic;
                    g.DrawImage(original.Clone(tagArea, original.PixelFormat), 0, 0, thumbnailWidth, thumbnailHeight);
                    g.Dispose();

                    // Save the thumbnail.
                    thumbnails[accountLinkButton] = thumbnail;
                }

                original.Dispose();
            }

            e.Result = thumbnails;
        }

        /// <summary>
        /// Once the background worker returns, the UI thread runs this function. If there was no
        /// error, the generated thumbnail is shown.
        /// </summary>
        private void thumbnailWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // Make sure there was no exception and that the background thread wasn't cancelled.
            if (e.Error == null && !e.Cancelled)
            {
                Dictionary<FacebookAccountLinkPanel, Bitmap> thumbnails = 
                    e.Result as Dictionary<FacebookAccountLinkPanel, Bitmap>;
                
                if (thumbnails == null)
                {
                    return;
                }

                // The UI thread needs to set the thumbnails into the buttons.
                foreach (KeyValuePair<FacebookAccountLinkPanel, Bitmap> thumbnailPair in thumbnails)
                {
                    thumbnailPair.Key.PersonTagSquarePicture.Image = thumbnailPair.Value;
                }
            }
        }

        /// <summary>
        /// This event is fired when the cancel button is clicked.
        /// </summary>
        private void cancelButton_Click(object sender, EventArgs e)
        {
            CloseForm(DialogResult.Cancel);
        }

        /// <summary>
        /// Closes the form with the given result.
        /// </summary>
        /// <param name="dialogResult">
        /// The return value of the form.
        /// </param>
        private void CloseForm(DialogResult dialogResult)
        {
            this.DialogResult = dialogResult;
            this.Close();
        }

        /// <summary>
        /// This event is fired when the next button on the chooseAccount panel is clicked.
        /// </summary>
        private void chooseAccountNextButton_Click(object sender, EventArgs e)
        {
            this.chooseAccountNextButton.Enabled = false;

            if (this.selectedUser != null)
            {
                if (uploadingVideos && !selectedUser.Permissions.CanUploadVideos)
                {
                    if (!AuthorizeVideoUploadPermission())
                    {
                        // None shall pass.
                        this.chooseAccountNextButton.Enabled = true;
                        return;
                    }
                }

                if (uploadingImages)
                {
                    // The user needs to choose an album.
                    PopulateChooseAlbumFlowLayoutPanel();
                    ShowPanel(ConfigurationSettingsPanel.ChooseAlbum);
                    PopulateLinkAccountsLayoutPanel();
                }
                else
                {
                    ShowPanel(ConfigurationSettingsPanel.VideoDesc);
                }
            }
            else
            {
                ShowPanel(ConfigurationSettingsPanel.PreAuth);
            }

            this.chooseAccountNextButton.Enabled = true;
        }

        /// <summary>
        /// Pops a dialog requesting that the user authorize Facebook's photo_upload permission.
        /// </summary>
        /// <returns>
        /// true if the user accepts the request and false otherwise.
        /// </returns>
        private bool AuthorizePhotoUploadPermission()
        {
            // Tell the user they need to authorize the extended video_upload permission.
            DialogResult result = MessageBox.Show(this, Resources.FacebookNeedsPhotoUploadPermission,
                Resources.PluginTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);

            if (result == DialogResult.Cancel)
            {
                return false;
            }

            // Send the user off to get the extended photo_upload permission.
            FacebookPublishPlugin.OpenDefaultBrowser(FacebookService.GetPhotoUploadPermissionUrl());

            return true;
        }

        /// <summary>
        /// Pops a dialog requesting that the user authorize Facebook's video_upload permission.
        /// </summary>
        /// <returns>
        /// true if the user accepts the request and false otherwise.
        /// </returns>
        private bool AuthorizeVideoUploadPermission()
        {
            // Tell the user they need to authorize the extended video_upload permission.
            DialogResult result = MessageBox.Show(this, Resources.FacebookNeedsVideoUploadPermission,
                Resources.PluginTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);

            if (result == DialogResult.Cancel)
            {
                return false;
            }

            // Send the user off to get the extended video_upload permission.
            FacebookPublishPlugin.OpenDefaultBrowser(FacebookService.GetVideoUploadPermissionUrl());

            return true;
        }

        /// <summary>
        /// Pops a dialog requesting that the user authorize Facebook's offline_access permission.
        /// </summary>
        /// <returns>
        /// true if the user accepts the request and false otherwise.
        /// </returns>
        private bool AuthorizeOfflineAccessPermission()
        {
            // Tell the user they need to authorize the extended video_upload permission.
            DialogResult result = MessageBox.Show(this, Resources.FacebookNeedsOfflineAccessPermission,
                Resources.PluginTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);

            if (result == DialogResult.Cancel)
            {
                return false;
            }

            // Send the user off to get the extended photo_upload permission.
            FacebookPublishPlugin.OpenDefaultBrowser(FacebookService.GetOfflineAccessPermissionUrl());

            return true;
        }

        /// <summary>
        /// This event is fired when the back button on the preAuth panel is clicked.
        /// </summary>
        private void preAuthBackButton_Click(object sender, EventArgs e)
        {
            this.preAuthBackButton.Enabled = false;

            // Go back to the chooseAccount panel.
            ShowPanel(ConfigurationSettingsPanel.ChooseAccount);

            this.preAuthBackButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the next button on the preAuth panel is clicked.
        /// </summary>
        private void preAuthNextButton_Click(object sender, EventArgs e)
        {
            this.preAuthNextButton.Enabled = false;

            try
            {
                // Get an authentication token from Facebook.
                this.authToken = FacebookService.CreateAuthToken();

                // Send the user to the Facebook login page using the authentication token.
                FacebookPublishPlugin.OpenDefaultBrowser(FacebookService.GetAuthenticationUrl(authToken));

                // Go on to the postAuth panel.
                ShowPanel(ConfigurationSettingsPanel.PostAuth);
            }
            catch (FacebookException fe)
            {
                Trace.WriteLine("Failed pre-authorization with error:");
                Trace.Indent();
                Trace.WriteLine(fe);
                Trace.Unindent();

                // The authentication token was not created.
                DisplayError(Resources.FacebookCreateTokenError, false);
            }
            finally
            {
                this.preAuthNextButton.Enabled = true;
            }
        }

        /// <summary>
        /// This event is fired when the back button on the postAuth panel is clicked.
        /// </summary>
        private void postAuthBackButton_Click(object sender, EventArgs e)
        {
            this.postAuthBackButton.Enabled = false;

            // Go on to the preAuth panel.
            ShowPanel(ConfigurationSettingsPanel.PreAuth);
            
            this.postAuthBackButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the next button on the postAuth panel is clicked.
        /// </summary>
        private void postAuthNextButton_Click(object sender, EventArgs e)
        {
            this.postAuthNextButton.Enabled = false;

            try
            {
                // Get the authenticated session from Facebook after the user has logged in.
                AccountSession session = FacebookService.GetAuthSession(this.authToken);

                // Now that we have an authenticated session, get information from Facebook about 
                // the logged in user.
                Account user = FacebookService.GetUserInfo(session);
                
                // Save and select the user.
                AddUserToUserList(user);
                SetSelectedUser(user);
                PopulateChooseAccountFlowLayoutPanel();

                if (!user.Permissions.CanAccessOffline)
                {
                    // Ask to remember.
                    AuthorizeOfflineAccessPermission();
                }

                if (!user.Permissions.CanUploadPhotos)
                {
                    // Ask for the permission.
                    AuthorizePhotoUploadPermission();
                }

                if (uploadingVideos)
                {
                    if (!user.Permissions.CanUploadVideos && !AuthorizeVideoUploadPermission())
                    {
                        // If the user refuses to authorize the video_upload permission, don't let them get any further.
                        this.postAuthNextButton.Enabled = true;
                        ShowPanel(ConfigurationSettingsPanel.ChooseAccount);
                        return;
                    }
                }
                
                if (uploadingImages)
                {
                    // Go on to the chooseAlbum panel.
                    PopulateChooseAlbumFlowLayoutPanel();
                    ShowPanel(ConfigurationSettingsPanel.ChooseAlbum);
                    PopulateLinkAccountsLayoutPanel();
                }
                else
                {
                    ShowPanel(ConfigurationSettingsPanel.VideoDesc);
                }
            }
            catch (FacebookException fe)
            {
                Trace.WriteLine("Failed post-authorization with error:");
                Trace.Indent();
                Trace.WriteLine(fe);
                Trace.Unindent();

                // We were not able to get either the authenticated session or user information.
                DisplayError(Resources.FacebookGetSessionError, false);
                
                // Go back to the preAuth panel to try again.
                ShowPanel(ConfigurationSettingsPanel.PreAuth);
            }
            finally
            {
                this.postAuthNextButton.Enabled = true;
            }
        }

        /// <summary>
        /// This event is fired when the back button on the chooseAlbum panel is clicked.
        /// </summary>
        private void chooseAlbumBackButton_Click(object sender, EventArgs e)
        {
            this.chooseAlbumBackButton.Enabled = false;

            // Go back to the chooseAccount panel.
            ShowPanel(ConfigurationSettingsPanel.ChooseAccount);

            this.chooseAlbumBackButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the next button on the chooseAlbum panel is clicked.
        /// </summary>
        private void chooseAlbumNextButton_Click(object sender, EventArgs e)
        {
            if (this.chooseAlbumCreateNew.Checked)
            {
                // Make sure we have the most up-to-date copy of the album.
                SetSelectedAlbum(this.chooseAlbumCreateNew.Album);
            }

            this.chooseAccountNextButton.Enabled = false;

            if (this.sessionData.UniquePeopleTags.Count > 0)
            {
                ShowPanel(ConfigurationSettingsPanel.LinkAccounts);
            }
            else if (uploadingVideos)
            {
                ShowPanel(ConfigurationSettingsPanel.VideoDesc);
            }
            else
            {
                ShowPanel(ConfigurationSettingsPanel.Publish);
            }

            this.chooseAccountNextButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the back button on the linkAccounts panel is clicked.
        /// </summary>
        private void linkAccountsBackButton_Click(object sender, EventArgs e)
        {
            this.linkAccountsBackButton.Enabled = false;

            ShowPanel(ConfigurationSettingsPanel.ChooseAlbum);

            this.linkAccountsBackButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the next button on the linkAccounts panel is clicked.
        /// </summary>
        private void linkAccountsNextButton_Click(object sender, EventArgs e)
        {
            this.linkAccountsNextButton.Enabled = false;

            if (uploadingVideos)
            {
                ShowPanel(ConfigurationSettingsPanel.VideoDesc);
            }
            else
            {
                ShowPanel(ConfigurationSettingsPanel.Publish);
            }

            this.linkAccountsNextButton.Enabled = true;
        }

        /// <summary>
        /// Fills the VideodDesc panel with an input for each video to be uploaded
        /// </summary>
        private void PopulateVideoDescLayoutPanel()
        {
            // This function may get called multiple times, so clear any existing controls.
            this.videoDescLayoutPanel.Controls.Clear();

            // Add top spacer
            Control spacer = CreateSpacer(TOP_PADDING);
            this.videoDescLayoutPanel.Controls.Add(spacer);
            this.videoDescLayoutPanel.SetFlowBreak(spacer, true);

            List<FacebookVideoPanel> videoButtons = new List<FacebookVideoPanel>();

            foreach (Item mediaObject in this.sessionData.ItemSet)
            {
                if (mediaObject.PerceivedType == "video")
                {
                    FacebookVideoPanel videoButton = new FacebookVideoPanel();
                    videoButton.Video = mediaObject;
                    videoButtons.Add(videoButton);
                    this.videoDescLayoutPanel.Controls.Add(videoButton);
                }
            }

            // Add bottom spacer
            spacer = CreateSpacer(BOTTOM_PADDING);
            this.videoDescLayoutPanel.Controls.Add(spacer);
            this.videoDescLayoutPanel.SetFlowBreak(spacer, true);
        }

        /// <summary>
        /// This event is fired when the back button on the videoDesc panel is clicked.
        /// </summary>
        private void videoDescBackButton_Click(object sender, EventArgs e)
        {
            this.videoDescBackButton.Enabled = false;

            if (!uploadingImages)
            {
                ShowPanel(ConfigurationSettingsPanel.ChooseAccount);
            }
            else if (this.sessionData.UniquePeopleTags.Count > 0)
            {
                ShowPanel(ConfigurationSettingsPanel.LinkAccounts);
            }
            else
            {
                ShowPanel(ConfigurationSettingsPanel.ChooseAlbum);
            }

            this.videoDescBackButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the next button on the videoDesc panel is clicked.
        /// </summary>
        private void videoDescNextButton_Click(object sender, EventArgs e)
        {
            this.videoDescNextButton.Enabled = false;

            ShowPanel(ConfigurationSettingsPanel.Publish);

            this.videoDescNextButton.Enabled = true;
        }

        /// <summary>
        /// Loads anything necessary for the Publish panel.
        /// </summary>
        private void PopulatePublishPanel()
        {
            int numImages = 0;
            int numVideos = 0;

            foreach (Item item in this.sessionData.ItemSet)
            {
                if (item.PerceivedType == "image")
                {
                    numImages++;
                }
                else if (item.PerceivedType == "video")
                {
                    numVideos++;
                }
            }

            string mediaCount = string.Empty;

            if (numImages > 0)
            {
                mediaCount += numImages.ToString() + (numImages == 1 ? " photo" : " photos");
            }
            if (numImages > 0 && numVideos > 0)
            {
                mediaCount += " and ";
            }
            if (numVideos > 0)
            {
                mediaCount += numVideos.ToString() + (numVideos == 1 ? " video" : " videos");
            }

            this.publishLabel.Text = string.Format(this.publishLabel.Text, mediaCount);
        }

        /// <summary>
        /// This event is fired when the back button on the publish panel is clicked.
        /// </summary>
        private void publishBackButton_Click(object sender, EventArgs e)
        {
            this.publishBackButton.Enabled = false;

            if (uploadingVideos)
            {
                ShowPanel(ConfigurationSettingsPanel.VideoDesc);
            }
            else if (this.sessionData.UniquePeopleTags.Count > 0)
            {
                ShowPanel(ConfigurationSettingsPanel.LinkAccounts);
            }
            else
            {
                ShowPanel(ConfigurationSettingsPanel.ChooseAlbum);
            }

            this.publishBackButton.Enabled = true;
        }

        /// <summary>
        /// This event is fired when the next button on the publish panel is clicked.
        /// </summary>
        private void publishNextButton_Click(object sender, EventArgs e)
        {
            this.publishNextButton.Enabled = false;

            // Save all the current settings and start uploading photos.
            Publish();

            this.publishNextButton.Enabled = true;
        }

        /// <summary>
        /// Saves all the current settings and closes the form with a successful return value.
        /// </summary>
        private void Publish()
        {
            if (uploadingVideos && !ShouldContinueVideoUpload())
            {
                return;
            }

            // Save the current user and album.
            this.sessionData.User = this.selectedUser;

            if (uploadingImages)
            {
                this.sessionData.Album = this.selectedAlbum;
                this.sessionData.FriendList = this.selectedUser.FriendList;

                // Save the new account links from the linkAccountsLayoutPanel.
                foreach (Control control in this.linkAccountsLayoutPanel.Controls)
                {
                    FacebookAccountLinkPanel accountLinkButton = control as FacebookAccountLinkPanel;
                    if (accountLinkButton != null)
                    {
                        if (accountLinkButton.AccountLink.FacebookFriend == null)
                        {
                            accountLinkButton.AccountLink.FacebookFriend = new Friend(accountLinkButton.FacebookFriendName, string.Empty, string.Empty);
                        }

                        int linkIndex = this.persistData.AccountLinks.FindIndex(delegate(AccountLink accountLinkMatch)
                        {
                            return accountLinkButton.AccountLink.PersonTag.Name == accountLinkMatch.PersonTag.Name;
                        });

                        if (linkIndex >= 0)
                        {
                            this.persistData.AccountLinks[linkIndex] = accountLinkButton.AccountLink;
                        }
                        else
                        {
                            this.persistData.AccountLinks.Add(accountLinkButton.AccountLink);
                        }
                    }
                }

                this.sessionData.AccountLinks = this.persistData.AccountLinks;

                // Try to create the album if it is not already created.
                if (!sessionData.Album.IsCreated())
                {
                    sessionData.Album = CreateAlbum(sessionData.User, sessionData.Album);

                    // If the creation was unsuccessful, stop and let the user try again.
                    if (sessionData.Album == null)
                    {
                        return;
                    }
                }
            }

            // Save some bandwidth by transcoding all files to the maximum dimension used by Facebook.
            this.sessionData.MaxWidth = FacebookService.MaxDimension;
            this.sessionData.MaxHeight = FacebookService.MaxDimension;

            // Write the session and application data to disk.
            this.sessionData.Save();
            this.persistData.Save();

            // Return successfully.
            CloseForm(DialogResult.OK);
        }

        private bool ShouldContinueVideoUpload()
        {
            bool videoOverLimit = false;

            foreach (Item mediaObject in this.sessionData.ItemSet)
            {
                if (mediaObject.PerceivedType == "video" && 
                    mediaObject.FileSize > this.selectedUser.VideoUploadLimits.Size)
                {
                    videoOverLimit = true;
                }
            }

            if (videoOverLimit)
            {
                string warningMessage = string.Format(Resources.VideoUploadLimitsWarning, 
                    this.selectedUser.VideoUploadLimits);
                DialogResult result = MessageBox.Show(warningMessage, 
                    Resources.PluginTitle, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);

                return result == DialogResult.OK;
            }

            return true;
        }

        /// <summary>
        /// Trys to create an uncreated album.
        /// </summary>
        /// <param name="user">
        /// The user who will own the album.
        /// </param>
        /// <param name="album">
        /// The uncreated album.
        /// </param>
        /// <returns>
        /// A newly created album or null.
        /// </returns>
        private Album CreateAlbum(Account user, Album album)
        {
            Album createdAlbum = null;

            try
            {
                // Create the album on Facebook.
                createdAlbum = FacebookService.CreateAlbum(user, album);

                if (createdAlbum != null && createdAlbum.IsCreated())
                {
                    // Log the successful album creation.
                    string message = string.Format(Resources.CreateAlbumSuccess, createdAlbum.Name);
                    sessionData.Log.Add(new LogItem(LogItem.Outcome.Successful, message));
                }
            }
            catch (FacebookException fe)
            {
                // Display an error message to the user.
                string errorMessage = string.Format(Resources.FacebookCreateAlbumError, album.Name);
                DisplayError(errorMessage, false);

                Trace.WriteLine("Failed creating album with error:");
                Trace.Indent();
                Trace.WriteLine(fe);
                Trace.Unindent();

                // Log the unsuccessful album creation.
                string error = string.Format(Resources.CreateAlbumFailure, album.Name, fe.Message);
                sessionData.Log.Add(new LogItem(LogItem.Outcome.Unsuccessful, error));

                return null;
            }

            return createdAlbum;
        }

        #endregion

    }
}
